28. 农产品交易可视化性分析

本章概要

  • 本章安排:先用教学平台内置的农产品交易数据完成固定练习,再用课程数据下载入口提供的农业类上市公司数据练习分布、趋势和关系分析。
  • 学习材料:农产品订单日期、品类、数量、单价、金额与订单键。
  • 本章任务:执行质量核对,计算三变量分布统计,再从同一快照聚合月/季度。
  • 完成后你将得到:行数守恒表、count/mean/median/std/skew和月季销售表。
  • 自我检查:检查原始=保留+移除及季度=月度求和;缺列、空样本或日期失败时先检查原因。
  • 拓展练习:把时间粒度拓展应用到另一农产品品类。

本章学习目标

  • 掌握农产品电商数据的探索性数据分析(EDA)方法
  • 学会使用箱线图、直方图、饼图等进行数据可视化
  • 理解缺失值检测、异常值识别等数据质量检查技术
  • 掌握时间特征提取与周末效应分析
  • 学会价格-销量关系分析与产品共现分析

核心与扩展路线

单元 核心目标(每单元≤3项) 中途检查
A|质量 字段、缺失、重复 写出清洗前后核对
B|分布 中心、离散、形态 用统计量核对一张分布图
C|时间 月度聚合、季节性、边界 改变时间粒度做敏感性

价格弹性与产品共现为课后挑战:必须有交易级授权数据与因果识别设计才可形成业务结论;不完成挑战不影响核心学习要求。

核心单元A|作答→反馈

先作答:列出交易表六个必需字段,并写出清洗前后行数核对式。
反馈:合格结果含 order_id/order_date/category/quantity/price/sales_amount,且满足“原始行数=清洗行数+移除行数”;缺字段或清洗后为空即停止。

核心单元B|作答→反馈

先作答:选择一个数值字段,用中心、离散、形态三类统计量解释一张分布图。
反馈:均值/中位数回答中心,标准差回答离散,偏度回答形态;异常原因不能由偏度或箱线图单独推出。

核心单元C|作答→反馈

先作答:把订单聚合为月度表,再改为季度表,预测行数和峰值月份是否变化。
反馈:两表必须来自同一清洗快照并报告订单分母;粒度改变会合并波动,不能把聚合差异解释为季节因果。

课后挑战|不计入章末练习

价格弹性需价格外生变动或识别设计,产品共现需交易级授权和重复购买定义;挑战答案须另报假设、偏差与需要重新检查的情况,不能替代核心质量—分布—时间依据。

农产品电商的数据价值

农产品电子商务的快速发展产生了海量交易数据,通过可视化分析可以:

  • 识别季节性需求和价格波动模式
  • 发现区域消费偏好差异
  • 优化供应链和库存管理
  • 制定精准营销策略

探索性数据分析(EDA)的四大原则

EDA 由统计学家 John Tukey 于 1977 年提出,核心思想是在正式建模前理解数据特征:

原则 说明
怀疑精神 对数据和假设保持质疑
可视化优先 图形比数字更直观
计算支撑 用统计量验证视觉发现
迭代探索 分析是循环往复的过程

农产品数据的特殊性

农产品数据与一般商品数据相比,具有四大显著特征:

  • 季节性:生产受自然周期影响,供给随季节波动
  • 地域性:不同地区消费习惯差异大
  • 易腐性:保鲜期短,库存管理至关重要
  • 价格波动:受供需关系和政策影响明显

数据与学习步骤

  • processed_data.csv 由教学平台提供,用于固定练习;公开教材的拓展案例使用课程下载的上市公司基本信息和财务报表。
  • 建议学习顺序:确认样本量、期间、字段和单位→完成探索性分析→手工重新计算两组汇总结果→说明结果及其适用范围。
  • 平台数据用于学习方法;拓展案例使用公开数据,结果只解释课程中实际分析的公司和期间。

任务准备|平台任务解答代码

以下代码与教学平台任务要求完全一致:

阶段 学习步骤 应得到什么
1 数据要求 核对文件、字段、期间、单位与授权 数据字典与行数
2 质量检查 检查缺失、重复、类型与极端值 质量报告
3 单变量分布 描述年龄、品类、渠道等分布 带分母的统计表

任务准备(续)|时间与关系验证

阶段 学习步骤 应得到什么
4 时间结构 检查趋势、季节性与断点 时间图与窗口说明
5 关系验证 先可视化,再用统计量核对 观测、假设与局限

运行前预测|平台任务解答代码

  • 输入预测:运行前先写出 datafeature_mapcategory_mappingorder 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到“查看数据集的基本信息:”相关结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务解答代码”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务解答代码

展开完整代码(投影默认折叠)
# 注:processed_data.csv数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 导入必要的数据分析和可视化库
import pandas as pd
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
import seaborn as sns  # 导入Seaborn可视化库
import warnings  # 导入warnings模块用于控制警告输出
warnings.filterwarnings('ignore')  # 忽略运行时警告信息以保持输出整洁
plt.rcParams['font.sans-serif'] = ['SimHei']  # 使用黑体作为默认字体以支持中文
plt.rcParams['axes.unicode_minus'] = False  # 正确显示负号

# 读取处理好的数据集
data = pd.read_csv('processed_data.csv')

# 查看数据集的基本情况
print(data.head())  # 显示数据集前几行
print("查看数据集的基本信息:")  # 输出查看数据集的基本信息
print(data.info())  # 显示数据集的基本信息,包括列名、数据类型和非空值数量
print("查看数据集有无缺失值:")  # 输出查看数据集有无缺失值
print(data.isnull().sum())  # 统计每列的缺失值数量
print("查看数据集有无重复值:")  # 输出查看数据集有无重复值
print(data.duplicated().sum())  # 统计重复行的数量

# 创建特征映射字典,将英文列名映射为中文名称,用于图表标题
feature_map = {
    'price': '价格',  # 映射"price"→"价格"
    'quantity': '数量',  # 映射"quantity"→"数量"
    'customer_age': '顾客年龄',  # 映射"customer_age"→"顾客年龄"
    'return_flag': '是否返单',  # 映射"return_flag"→"是否返单"
    'sales_amount': '销售额'  # 映射"sales_amount"→"销售额"
}  # 数据结构定义结束

# 创建箱型图展示各特征的分布情况
plt.figure(figsize=(20,10))  # 设置图形大小
for i, feature in enumerate(feature_map.keys()):  # 循环遍历
    plt.subplot(2,3, i+1)  # 创建2行3列的子图布局
    plt.title(f'{feature_map[feature]}的箱型图')  # 设置子图标题
    plt.boxplot(data[feature])  # 绘制箱线图
    plt.xlabel(f'{feature_map[feature]}')  # 设置x轴标签
    plt.ylabel('频率')  # 设置y轴标签
plt.savefig("特征箱型图.png")  # 保存图形

# 创建一个字典来映射产品名称到正确的分类
category_mapping = {
    '安溪铁观音': '茶叶',  # 映射"安溪铁观音"→"茶叶"
    '武夷岩茶': '茶叶',  # 映射"武夷岩茶"→"茶叶"
    '福州茉莉花': '茶叶',  # 映射"福州茉莉花"→"茶叶"
    '古田银耳': '食用菌',  # 映射"古田银耳"→"食用菌"
    '建宁莲子': '中药材',  # 映射"建宁莲子"→"中药材"
    '琯溪蜜柚': '水果',  # 映射"琯溪蜜柚"→"水果"
    '宁德大黄鱼': '水产品'  # 映射"宁德大黄鱼"→"水产品"
}  # 数据结构定义结束

# 应用映射来更新category列,将产品名称转换为产品类别
data['category'] = data['product_name'].map(category_mapping).fillna(data['category'])

# 2.可视化展示 - 计算并转置描述性统计数据(结果未打印,仅计算)
data.describe().T

# 2.1 顾客信息可视化
plt.figure(figsize=(20,10))  # 设置图形大小

# 顾客年龄分布直方图
plt.subplot(2,2,1)
sns.distplot(data['customer_age'], bins=30, kde=True, color='blue')  # 绘制带核密度估计的直方图
plt.title('顾客年龄分布', fontsize=16)  # 设置标题
plt.xlabel('顾客年龄', fontsize=12)  # 设置x轴标签
plt.ylabel('频率', fontsize=12)  # 设置y轴标签

# 顾客年龄组别分布柱状图
plt.subplot(2,2,2)
order = ['18-25', '26-35', '36-45', '46-55', '56+']  # 自定义年龄组顺序
sns.countplot(data=data, x='age_group', color='orange', order=order)  # 按指定顺序绘制柱状图
plt.title('顾客年龄组别分布', fontsize=16)  # 设置标题
plt.xlabel('顾客年龄组别', fontsize=12)  # 设置x轴标签
plt.ylabel('频率', fontsize=12)  # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
    plt.gca().annotate(format(p.get_height(), '.0f'),  # 获取当前坐标轴并在柱状图顶部标注数值
                       (p.get_x() + p.get_width() / 2., p.get_height()),  # 设置标注位置为柱子顶部中心
                       # 设置标注文本的水平对齐、垂直偏移等显示参数
                       ha='center', va='center', xytext=(0, 5), textcoords='offset points')

# 顾客性别分布柱状图
plt.subplot(2,2,3)
sns.countplot(data=data, x='customer_gender', color='green')  # 绘制性别分布柱状图
plt.title('顾客性别分布', fontsize=16)  # 设置标题
plt.xlabel('顾客性别', fontsize=12)  # 设置x轴标签
plt.ylabel('频率', fontsize=12)  # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
    plt.gca().annotate(format(p.get_height(), '.0f'),  # 获取当前坐标轴并在柱状图顶部标注数值
                       (p.get_x() + p.get_width() / 2., p.get_height()),  # 设置标注位置为柱子顶部中心
                       # 设置标注文本的水平对齐、垂直偏移等显示参数
                       ha='center', va='center', xytext=(0, 5), textcoords='offset points')

# 顾客性别比例饼图
plt.subplot(2,2,4)
plt.pie(data['customer_gender'].value_counts(),  # 绘制饼图
        labels=data['customer_gender'].value_counts().index,  # 统计各个值的出现频次
        autopct='%1.1f%%', colors=['green', 'orange'])  # 绘制饼图并显示百分比
plt.title('顾客性别比例', fontsize=16)  # 设置标题
plt.axis('equal')  # 确保饼图是圆形
plt.savefig("顾客信息.png")  # 保存图形
#运行后依据计数表报告年龄组别样本量与占比;当前不预填结论
#运行后依据计数表报告性别样本量、占比与缺失情况;当前不预填结论

# 区域分布可视化
plt.figure(figsize=(20,5))  # 设置图形大小
sns.countplot(x='region', data=data, palette='Set2')  # 绘制计数柱状图
plt.title('区域分布', fontsize=20)  # 设置标题
plt.xlabel('区域', fontsize=16)  # 设置x轴标签
plt.ylabel('数量', fontsize=16)  # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
    plt.gca().annotate(format(p.get_height(), '.0f'),  # 获取当前坐标轴并在柱状图顶部标注数值
                       (p.get_x() + p.get_width() / 2., p.get_height()),  # 设置标注位置为柱子顶部中心
                       # 设置标注文本的水平对齐、垂直偏移等显示参数
                       ha='center', va='center', xytext=(0, 5), textcoords='offset points')
plt.savefig("区域分布.png")  # 保存图形

# 2.2 商品信息可视化
plt.figure(figsize=(20,5))  # 设置图形大小

# 产品名称分布柱状图
plt.subplot(1, 2, 1)
sns.countplot(x='product_name', data=data, palette='Set2')  # 绘制产品名称分布柱状图
plt.xlabel('产品名称', fontsize=16)  # 设置x轴标签
plt.ylabel('数量', fontsize=16)  # 设置y轴标签
plt.title('产品分布', fontsize=20)  # 设置标题
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
    plt.gca().annotate(format(p.get_height(), '.0f'),  # 获取当前坐标轴并在柱状图顶部标注数值
                       (p.get_x() + p.get_width() / 2., p.get_height()),  # 设置标注位置为柱子顶部中心
                       # 设置标注文本的水平对齐、垂直偏移等显示参数
                       ha='center', va='center', xytext=(0, 5), textcoords='offset points')

# 产品类型分布柱状图
plt.subplot(1, 2, 2)
sns.countplot(x='category', data=data, palette='Set2')  # 绘制产品类型分布柱状图
plt.xlabel('产品类型', fontsize=16)  # 设置x轴标签
plt.ylabel('数量', fontsize=16)  # 设置y轴标签
plt.title('产品类型分布', fontsize=20)  # 设置标题
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
    plt.gca().annotate(format(p.get_height(), '.0f'),  # 获取当前坐标轴并在柱状图顶部标注数值
                       (p.get_x() + p.get_width() / 2., p.get_height()),  # 设置标注位置为柱子顶部中心
                       # 设置标注文本的水平对齐、垂直偏移等显示参数
                       ha='center', va='center', xytext=(0, 5), textcoords='offset points')
plt.savefig("商品信息.png")  # 保存图形
#总体而言,茶叶是销量最多的产品类型,远超其他产品类型。
#具体产品分布上,销量最多的产品是古田银耳,共1474单;其次是安溪铁观音,共1461单;第三名是宁德大黄鱼,共1438单。

# 销售相关指标分布可视化
plt.figure(figsize=(20,6))  # 设置图形大小

# 价格分布直方图
plt.subplot(1, 3, 1)
sns.distplot(data['price'], kde=True, color='blue')  # 绘制价格分布直方图
plt.title('价格分布', fontsize=16)  # 设置标题
plt.xlabel('价格', fontsize=12)  # 设置x轴标签
plt.ylabel('频率', fontsize=12)  # 设置y轴标签

# 数量分布直方图
plt.subplot(1, 3, 2)
sns.distplot(data['quantity'], kde=True, color='green')  # 绘制数量分布直方图
plt.title('数量分布', fontsize=16)  # 设置标题
plt.xlabel('数量', fontsize=12)  # 设置x轴标签
plt.ylabel('频率', fontsize=12)  # 设置y轴标签

# 销售额分布直方图
plt.subplot(1, 3, 3)
sns.distplot(data['sales_amount'], kde=True, color='orange')  # 绘制销售额分布直方图
plt.title('销售额分布', fontsize=16)  # 设置标题
plt.xlabel('销售额', fontsize=12)  # 设置x轴标签
plt.ylabel('频率', fontsize=12)  # 设置y轴标签
plt.savefig("销售.png")  # 保存图形

# 2.3 平台及优惠信息可视化
plt.figure(figsize=(20,12))  # 设置图形大小

# 平台分布柱状图
plt.subplot(2, 3, 1)
sns.countplot(x='channel', data=data, palette='Set2')  # 绘制平台分布柱状图
plt.title('平台分布', fontsize=16)  # 设置标题
plt.xlabel('平台', fontsize=14)  # 设置x轴标签
plt.ylabel('频数', fontsize=14)  # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
    plt.gca().annotate(format(p.get_height(), '.0f'),  # 获取当前坐标轴并在柱状图顶部标注数值
                       (p.get_x() + p.get_width() / 2., p.get_height()),  # 设置标注位置为柱子顶部中心
                       # 设置标注文本的水平对齐、垂直偏移等显示参数
                       ha='center', va='center', xytext=(0, 5), textcoords='offset points')

# 平台比例饼图
plt.subplot(2, 3 ,2)
plt.pie(data['channel'].value_counts(),  # 绘制饼图
        labels=data['channel'].value_counts().index,  # 统计各个值的出现频次
        autopct='%1.1f%%')  # 绘制平台比例饼图
plt.title('平台比例', fontsize=16)  # 设置标题

# 优惠情况分布柱状图
plt.subplot(2,3, 3)
sns.countplot(x='promotion', data=data, palette='Set2')  # 绘制优惠情况分布柱状图
plt.title('优惠情况分布', fontsize=16)  # 设置标题
plt.xlabel('优惠情况', fontsize=14)  # 设置x轴标签
plt.ylabel('频数', fontsize=14)  # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
    plt.gca().annotate(format(p.get_height(), '.0f'),  # 获取当前坐标轴并在柱状图顶部标注数值
                       (p.get_x() + p.get_width() / 2., p.get_height()),  # 设置标注位置为柱子顶部中心
                       # 设置标注文本的水平对齐、垂直偏移等显示参数
                       ha='center', va='center', xytext=(0, 5), textcoords='offset points')

# 优惠情况比例饼图
plt.subplot(2, 3, 4)
plt.pie(data['promotion'].value_counts(),  # 绘制饼图
        labels=data['promotion'].value_counts().index,  # 统计各个值的出现频次
        autopct='%1.1f%%')  # 绘制优惠情况比例饼图
plt.title('优惠情况比例', fontsize=16)  # 设置标题

# 是否返单分布柱状图
plt.subplot(2, 3, 5)
sns.countplot(x='return_flag', data=data, palette='Set2')  # 绘制是否返单分布柱状图
plt.title('是否返单分布', fontsize=16)  # 设置标题
plt.xlabel('是否返单', fontsize=14)  # 设置x轴标签
plt.ylabel('频数', fontsize=14)  # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
    plt.gca().annotate(format(p.get_height(), '.0f'),  # 获取当前坐标轴并在柱状图顶部标注数值
                       (p.get_x() + p.get_width() / 2., p.get_height()),  # 设置标注位置为柱子顶部中心
                       # 设置标注文本的水平对齐、垂直偏移等显示参数
                       ha='center', va='center', xytext=(0, 5), textcoords='offset points')

# 是否返单比例饼图
plt.subplot(2, 3, 6)
plt.pie(data['return_flag'].value_counts(),  # 绘制饼图
        labels=data['return_flag'].value_counts().index,  # 统计各个值的出现频次
        autopct='%1.1f%%')  # 绘制是否返单比例饼图
plt.title('是否返单比例', fontsize=16)  # 设置标题
plt.savefig("平台及优惠信息.png")  # 保存图形
#运行后依据平台计数表报告排序、样本量与占比;当前不预填结论
#运行后依据优惠计数表报告各类别样本量与占比;当前不预填结论
#运行后依据返单计数表报告样本量、占比与缺失情况;当前不预填结论

任务复盘|平台任务解答代码

运行后核对:核对 datafeature_mapcategory_mappingorder 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把数据替换为一个中国企业或市场序列,改变一个分组或时间窗口;说明图形结论是否改变,以及为何。

第一部分:准备数据

数据读取与初步检查

数据预处理是分析的第一步,通过查看数据结构和基本统计信息来了解数据质量:

  • shape:返回数据维度(行数, 列数)
  • info():显示每列的数据类型和非空值数量
  • describe().T:转置描述统计表,更易阅读
  • isnull().sum():统计每列缺失值数量

数据读取代码详解

展开完整代码(投影默认折叠)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

# 设置中文显示
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']
plt.rcParams['axes.unicode_minus'] = False

# 读取数据并输出基本信息
data = pd.read_csv('processed_data.csv')
print('数据形状:', data.shape)
print('\n数据前5行:')
print(data.head())
print('\n数据信息:')
print(data.info())
print('\n描述统计:')
print(data.describe().T)

数据类型检查要点

在初步检查中,需要关注三类数据类型:

数据类型 示例字段 说明
数值型 (int/float) 年龄、金额、数量 可直接参与计算
字符型 (object) 产品名称、类别 需要编码或分组分析
日期型 (datetime) 下单日期 需要格式转换后提取特征

中文显示配置说明

在 Matplotlib 中显示中文需要两步配置:

  • plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']:设置思源宋体为默认中文字体
  • plt.rcParams['axes.unicode_minus'] = False:修复负号显示问题

产品分类映射的意义

产品分类是电商数据分析的基础,农产品通常采用混合分类策略

  • 一级分类:茶叶、水果、水产品等大类
  • 二级分类:具体品种(铁观音、武夷岩茶)
  • 三级分类:产地、等级等细分

产品分类映射代码

展开完整代码(投影默认折叠)
# 产品名称到分类的映射字典
category_mapping = {
    '安溪铁观音': '茶叶',
    '武夷岩茶': '茶叶',
    '福州茉莉花': '茶叶',
    '古田银耳': '食用菌',
    '建宁莲子': '中药材',
    '琯溪蜜柚': '水果',
    '宁德大黄鱼': '水产品'
}

产品分类映射与核对


# map()根据字典映射,fillna()保留未匹配的值
data['category'] = data['product_name'].map(
    category_mapping
).fillna(data['category'])

# 验证映射结果
print('分类映射结果:')
print(data[['product_name', 'category']].drop_duplicates())
print('\n分类统计:')
print(data['category'].value_counts())

map() 函数的核心机制

  • map(dict):根据字典进行键值映射
  • fillna():处理未匹配的值(保留原分类)
  • for 循环映射高效 100 倍以上
  • drop_duplicates():去重查看唯一映射关系

第二部分:检查数据质量

缺失值的三种类型

缺失值分为三种:

类型 含义 处理方法
MCAR 完全随机缺失,与任何变量无关 直接删除或插补
MAR 缺失与观测变量相关 多重插补
MNAR 缺失与未观测变量相关 建模机制

缺失值检查与可视化代码

展开完整代码(投影默认折叠)
# 缺失值统计
missing_counts = data.isnull().sum()
missing_pct = (missing_counts / len(data)) * 100
missing_df = pd.DataFrame({
    '缺失数': missing_counts,
    '缺失比例': missing_pct
})
print(missing_df)

# 缺失值可视化
plt.figure(figsize=(10, 6))
missing_df['缺失比例'].plot(kind='bar', color='steelblue', alpha=0.7)
plt.title('各字段缺失值比例', fontsize=14)
plt.xlabel('字段名', fontsize=12)
plt.ylabel('缺失比例(%)', fontsize=12)
plt.xticks(rotation=45)
plt.axhline(y=5, color='red', linestyle='--', label='5%阈值')
plt.legend()
plt.tight_layout()
plt.show()

重复值检查与数据清洗

# 重复值检查
print(f'重复值数量: {data.duplicated().sum()}')
print(f'重复值比例: {data.duplicated().sum()/len(data)*100:.2f}%')

# 数据清洗:删除重复行
data_clean = data.drop_duplicates()
print(f'\n清洗后数据形状: {data_clean.shape}')
print(f'数据保留率: {len(data_clean)/len(data)*100:.2f}%')

异常值的识别方法

异常值 (Outlier) 指显著偏离其他观测值的数据点:

方法类别 具体方法 适用场景
基于统计量 3σ 原则、IQR 方法 正态/近似正态分布
基于可视化 箱线图、散点图 快速直观判断
基于模型 DBSCAN、孤立森林 高维复杂数据

IQR 异常值检测原理

IQR 方法的判定规则:

  • \(Q_1\):第一四分位数(25% 分位)
  • \(Q_3\):第三四分位数(75% 分位)
  • \(IQR = Q_3 - Q_1\):四分位距
  • 下界\(Q_1 - 1.5 \times IQR\)
  • 上界\(Q_3 + 1.5 \times IQR\)

超出上下界的数据点即为异常值。

异常值检测代码

展开完整代码(投影默认折叠)
# 选择数值型字段
numeric_cols = data_clean.select_dtypes(include=[np.number]).columns

# 绘制箱线图
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
axes = axes.flatten()
for i, col in enumerate(numeric_cols[:4]):
    data_clean.boxplot(column=col, ax=axes[i])
    axes[i].set_title(f'{col} - 箱线图', fontsize=12)
    axes[i].grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()

# IQR 异常值检测函数
def detect_outliers_iqr(series):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    return ((series < lower) | (series > upper)).sum()

# 输出异常值统计
print('\n异常值统计(IQR方法):')
for col in numeric_cols:
    outlier_count = detect_outliers_iqr(data_clean[col].dropna())
    print(f'{col}: {outlier_count}个异常值')

第三部分:分析数据分布

描述数据分布的三个维度

维度 指标 含义
集中趋势 均值、中位数、众数 数据的中心位置
离散程度 方差、标准差、极差 数据偏离中心的程度
分布形状 偏度、峰度 对称性和尖锐程度

偏度与峰度的解读

指标 负值 约为 0 正值
偏度 左尾更长 近似对称 右尾更长
超额峰度 尾部较轻 接近正态 尾部较重

判读边界:偏度不等于“存在异常值”,峰度也不等于因果机制;须结合直方图、箱线图与数据口径核对。

销售额分布分析代码

展开完整代码(投影默认折叠)
from scipy.stats import skew, kurtosis

sales = data_clean['sales_amount']

# 绘制分布图
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 直方图(含均值和中位数参考线)
axes[0].hist(sales, bins=50, color='steelblue', alpha=0.7,
             edgecolor='black')
axes[0].axvline(sales.mean(), color='red', linestyle='--',
                linewidth=2, label=f'均值={sales.mean():.2f}')
axes[0].axvline(sales.median(), color='green', linestyle='--',
                linewidth=2, label=f'中位数={sales.median():.2f}')
axes[0].set_title('销售额分布直方图', fontsize=14)
axes[0].set_xlabel('销售额(元)', fontsize=12)
axes[0].set_ylabel('频数', fontsize=12)
axes[0].legend(fontsize=10)

# 箱线图
axes[1].boxplot(sales, vert=True)
axes[1].set_title('销售额箱线图', fontsize=14)
axes[1].set_ylabel('销售额(元)', fontsize=12)

plt.tight_layout()
plt.show()

# 输出分布统计量
print(f'均值: {sales.mean():.2f}')
print(f'中位数: {sales.median():.2f}')
print(f'标准差: {sales.std():.2f}')
print(f'偏度: {skew(sales):.4f}')
print(f'峰度: {kurtosis(sales):.4f}')

品类分布分析代码

展开完整代码(投影默认折叠)
# 按品类聚合统计
category_sales = data_clean.groupby('category')['sales_amount'].agg([
    'sum', 'count', 'mean'
])
category_sales.columns = ['总销售额', '订单数', '平均单价']
category_sales = category_sales.sort_values('总销售额', ascending=False)
print('\n品类销售统计:')
print(category_sales)

# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 总销售额柱状图
category_sales['总销售额'].plot(kind='bar', ax=axes[0],
                               color='steelblue', alpha=0.7)
axes[0].set_title('各品类总销售额', fontsize=14)
axes[0].set_xlabel('产品类别', fontsize=12)
axes[0].set_ylabel('销售额(元)', fontsize=12)
axes[0].tick_params(axis='x', rotation=45)

# 订单数饼图
axes[1].pie(category_sales['订单数'],
            labels=category_sales.index,
            autopct='%1.1f%%', startangle=90,
            colors=sns.color_palette('Set3'))
axes[1].set_title('各品类订单数占比', fontsize=14)

plt.tight_layout()
plt.show()

品类分析的关键发现

数据可用后,按以下顺序识别品类结构:

  • 观测:报告各品类销售额、销量、订单数、毛利口径与观察期;当前不报告 Top 3 数值。
  • 假设:长尾可能源于需求、供货、价格或上架天数,排名不能单独确定原因。
  • 行动判断条件:品类经理只在连续两期毛利为负且缺货/供应异常已排除时启动调整试点,每月再次检查。

第四部分:分析时间变化

时间序列数据的四大特征

特征 说明 示例
趋势性 长期上升或下降趋势 年销售额逐年增长
季节性 固定周期的波动 春节前后销量高峰
周期性 不固定周期的波动 经济周期影响消费
随机性 不可预测的噪声 突发事件造成波动

时间特征提取代码

展开完整代码(投影默认折叠)
# 确保日期格式正确
data_clean['下单日期'] = pd.to_datetime(data_clean['order_date'])

# 提取时间特征
data_clean['年'] = data_clean['下单日期'].dt.year
data_clean['月'] = data_clean['下单日期'].dt.month
data_clean['日'] = data_clean['下单日期'].dt.day
data_clean['星期'] = data_clean['下单日期'].dt.dayofweek
data_clean['小时'] = data_clean['下单日期'].dt.hour

# 按月统计
monthly_sales = data_clean.groupby('月')['sales_amount'].agg([
    'sum', 'count'
])
monthly_sales.columns = ['月销售额', '月订单数']
print('月度销售统计:')
print(monthly_sales)

月度趋势可视化

展开完整代码(投影默认折叠)
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 月销售额折线图
monthly_sales['月销售额'].plot(ax=axes[0], marker='o', linewidth=2)
axes[0].set_title('月销售额趋势', fontsize=14)
axes[0].set_xlabel('月份', fontsize=12)
axes[0].set_ylabel('销售额(元)', fontsize=12)
axes[0].grid(True, alpha=0.3)

# 月订单数柱状图
monthly_sales['月订单数'].plot(kind='bar', ax=axes[1],
                               color='coral', alpha=0.7)
axes[1].set_title('月订单数分布', fontsize=14)
axes[1].set_xlabel('月份', fontsize=12)
axes[1].set_ylabel('订单数', fontsize=12)

plt.tight_layout()
plt.show()

周末效应分析

分析工作日与周末的消费差异,为营销和物流策略提供依据:

# 标记周末(5=周六, 6=周日)
data_clean['是否周末'] = data_clean['星期'].isin([5, 6])

# 周末 vs 工作日对比
weekend_comparison = data_clean.groupby('是否周末')[
    'sales_amount'
].agg(['mean', 'sum', 'count'])
weekend_comparison.index = ['工作日', '周末']
print('周末效应分析:')
print(weekend_comparison)

周末效应可视化

展开完整代码(投影默认折叠)
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 平均销售额对比
weekend_comparison['mean'].plot(kind='bar', ax=axes[0],
    color=['skyblue', 'orange'], alpha=0.7)
axes[0].set_title('平均销售额对比', fontsize=14)
axes[0].set_ylabel('平均销售额(元)', fontsize=12)
axes[0].tick_params(axis='x', rotation=0)

# 总销售额对比
weekend_comparison['sum'].plot(kind='bar', ax=axes[1],
    color=['skyblue', 'orange'], alpha=0.7)
axes[1].set_title('总销售额对比', fontsize=14)
axes[1].set_ylabel('总销售额(元)', fontsize=12)
axes[1].tick_params(axis='x', rotation=0)

plt.tight_layout()
plt.show()

第五部分:分析变量关系

价格弹性的经济学含义

价格弹性 (Price Elasticity) 衡量需求量对价格变化的敏感度:

\[ \large E_p = \frac{\%\Delta Q}{\%\Delta P} = \frac{\partial Q}{\partial P} \times \frac{P}{Q} \]

  • \(|E_p| > 1\):富有弹性(奢侈品)
  • \(|E_p| < 1\):缺乏弹性(必需品)
  • \(|E_p| = 1\):单位弹性

价格与销量关系分析代码

下方是不同产品之间的截面相关分析,不是价格弹性估计。产品异质性、促销、季节和供货可能共同影响价格与销量;需要同一产品的价格变化、控制变量和可辩识的价格冲击才能向弹性推进。

展开完整代码(投影默认折叠)
# 按产品统计价格与销量
product_stats = data_clean.groupby('product_name').agg({
    'sales_amount': 'sum',
    'quantity': 'sum',
    'unit_price': 'mean'
})
product_stats['单价'] = (product_stats['sales_amount']
                         / product_stats['quantity'])

# 计算相关系数
corr_coef = product_stats[['单价', 'quantity']].corr().iloc[0, 1]
print(f'价格与销量的相关系数: {corr_coef:.4f}')

价格-销量散点图与热力图

展开完整代码(投影默认折叠)
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 散点图
axes[0].scatter(product_stats['单价'],
                product_stats['quantity'], s=100, alpha=0.6)
axes[0].set_title(f'价格-销量散点图 (r={corr_coef:.4f})', fontsize=14)
axes[0].set_xlabel('单价(元)', fontsize=12)
axes[0].set_ylabel('销量', fontsize=12)
# 添加产品标签
for idx, row in product_stats.iterrows():
    axes[0].annotate(idx, (row['单价'], row['quantity']), fontsize=8)

# 热力图
sns.heatmap(product_stats[['单价', 'quantity']].corr(),
            annot=True, cmap='coolwarm', center=0, ax=axes[1],
            cbar_kws={'label': '相关系数'})
axes[1].set_title('相关性热力图', fontsize=14)

plt.tight_layout()
plt.show()

产品共现分析的意义

产品共现分析可以发现哪些产品经常被一起购买:

  • 捆绑销售:将高共现产品组合销售
  • 推荐系统:购买 A 产品的用户可能也想买 B
  • 货架摆放:将关联产品相邻放置

产品共现分析代码

展开完整代码(投影默认折叠)
from itertools import combinations

# 获取每个订单的产品列表
order_products = data_clean.groupby('order_id')[
    'product_name'
].apply(list)

# 统计产品对共现次数
co_occurrence = {}
for products in order_products:
    for combo in combinations(sorted(products), 2):
        co_occurrence[combo] = co_occurrence.get(combo, 0) + 1

# 转换为 DataFrame 并排序
co_df = pd.DataFrame(list(co_occurrence.items()),
                     columns=['产品对', '共现次数'])
co_df = co_df.sort_values('共现次数', ascending=False).head(10)
print('Top 10产品组合:')
print(co_df)

产品共现可视化

展开完整代码(投影默认折叠)
plt.figure(figsize=(12, 6))
plt.barh(range(len(co_df)), co_df['共现次数'],
         color='steelblue', alpha=0.7)
plt.yticks(range(len(co_df)),
           [f'{p1} + {p2}' for p1, p2 in co_df['产品对']])
plt.xlabel('共现次数', fontsize=12)
plt.title('产品共现分析(Top 10)', fontsize=14)
plt.grid(axis='x', alpha=0.3)
plt.tight_layout()
plt.show()

本章总结

本章核心知识回顾

分析环节 核心方法 关键工具
数据准备 分类映射、类型转换 map(), pd.to_datetime()
质量检查 缺失值、异常值检测 isnull(), IQR 方法
分布分析 偏度、峰度、直方图 scipy.stats, hist()
时间分析 月度趋势、周末效应 dt 属性、groupby()
多变量 相关分析、共现分析 corr(), combinations

关键业务发现

  • 结果说明:平台练习中的年龄、性别、品类、渠道、优惠和返单结果只解释该平台数据;公开教材的农业企业案例使用另一组字段,应单独解释。
  • 发布要求:每条发现必须同页给出样本量、期间、字段口径、实际表/图与一次另行核对。
  • 决策要求:由品类或渠道负责人预声明阈值和试点成本;毛利为负或客诉率上升超 2pp 即停,每月再次检查。

随堂练习

  • 问题 1|数据质量是否可靠?:写出六个必需字段、缺失/重复处理规则和“原始=保留+移除”行数核对;缺列、空样本或日期解析失败时先检查原因。
  • 问题 2|数据分布有什么特点?:为 quantity/price/sales_amount 输出 count、mean、median、std、skew,并用其中两个统计量核对一张分布图。
  • 问题 3|不同时间粒度下有何变化?:由同一清洗数据输出月度订单数与销售额,改用季度粒度复算并解释聚合差异;章末不要求分析价格弹性或产品共现。
  • 作答提示:请完成三道题,并在回答中引用实际运行结果;拓展练习中不要把课堂示例写成普遍规律或因果结论。

教师参考解答

教师参考解答|答案与说明 1

  • 所用数据与字段:农产品交易只使用平台提供的受保护表;适配受保护代码实际使用的 order_id/order_date/category/quantity/price/sales_amount
  • 平台练习答案|清洗、分布、时间与关系统计(平台保护块之外):

教师参考解答|代码 1

展开代码(代码区可独立滚动)
import pandas as pd  # 导入表格库
def analyze_platform_agriculture(agri_transactions):  # 定义平台农产品EDA完整接口
    required={'order_id','order_date','category','quantity','price','sales_amount'}  # 对齐平台实际字段
    if not required<=set(agri_transactions.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺列时暂停分析
    raw_rows=len(agri_transactions)  # 记录清洗前行数
    clean_transactions=agri_transactions.dropna(subset=list(required)).drop_duplicates().copy()  # 去重并排除关键字段缺失
    if clean_transactions.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 空样本时停止
    clean_transactions['order_date']=pd.to_datetime(clean_transactions['order_date'])  # 统一交易日期
    clean_transactions['month']=clean_transactions['order_date'].dt.to_period('M').astype(str)  # 构造月度键
    clean_transactions['quarter']=clean_transactions['order_date'].dt.to_period('Q').astype(str)  # 由同一清洗快照构造季度键
    if clean_transactions['month'].nunique()<2: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 时间样本不足时停止
    reconciliation={'raw_rows':raw_rows,'clean_rows':len(clean_transactions),'removed_rows':raw_rows-len(clean_transactions)}  # 形成清洗核对
    distribution=clean_transactions[['quantity','price','sales_amount']].agg(['count','mean','median','std','skew'])  # 汇总分布五指标
    monthly=clean_transactions.groupby('month').agg(orders=('order_id','nunique'),sales=('sales_amount','sum'))  # 生成时间趋势
    quarterly=clean_transactions.groupby('quarter').agg(orders=('order_id','nunique'),sales=('sales_amount','sum'))  # 用相同字段口径生成季度趋势
    relationship=clean_transactions[['quantity','price','sales_amount']].corr(method='spearman')  # 计算稳健秩相关
    category_check=clean_transactions.groupby('category')['sales_amount'].sum()  # 独立复算品类销售
    weekend_check=clean_transactions.assign(weekend=clean_transactions['order_date'].dt.dayofweek.ge(5)).groupby('weekend')['sales_amount'].agg(['count','sum','mean'])  # 复算周末与工作日
    assert reconciliation['raw_rows']==reconciliation['clean_rows']+reconciliation['removed_rows']  # 核对清洗行数守恒
    if monthly['orders'].eq(0).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 分母异常时停止
    assert monthly['sales'].sum()==quarterly['sales'].sum()==clean_transactions['sales_amount'].sum()  # 核对月季销售额与清洗快照总额守恒
    return reconciliation,distribution,monthly,quarterly,relationship,category_check,weekend_check  # 返回质量、分布与月季时间结果

教师参考解答|答案与说明 2

  • 中国本地解答|长三角农林牧渔上市公司分布拓展应用

教师参考解答|代码 2

展开代码(代码区可独立滚动)
def select_latest_disclosure(frame,value_fields):  # 统一财报披露版本选择约定
    required={'ts_code','end_date',*value_fields}; versions=[c for c in ['ann_date','f_ann_date'] if c in frame.columns]  # 固定键与可接受版本字段
    if not required<=set(frame.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺列时停止
    work=frame.dropna(subset=list(required)).copy(); work['end_date']=pd.to_datetime(work['end_date'],errors='coerce')  # 清理必需字段
    for column in versions: work[column]=pd.to_datetime(work[column],errors='coerce')  # 解析披露日期
    keys=['ts_code','end_date']; work=work.dropna(subset=['end_date']); duplicate_mask=work.duplicated(keys,keep=False); before=int(duplicate_mask.sum())  # 统计选择前重复
    if before and (not versions or work.loc[duplicate_mask,versions].isna().all(axis=1).any()): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 无版本依据时停止
    work['_version']=work[versions].max(axis=1) if versions else pd.NaT  # 合并双版本字段为排序键
    latest_version=work.groupby(keys)['_version'].transform('max') if versions else pd.Series(pd.NaT,index=work.index)  # 为每个公司报告期定位最晚披露时点
    latest_rows=work.loc[~duplicate_mask | work['_version'].eq(latest_version)].copy()  # 仅保留单例或并列最晚版本
    tied_mask=latest_rows.duplicated(keys,keep=False); tied_latest_rows=int(tied_mask.sum())  # 统计并列最晚版本行
    conflicting_latest_keys=int(latest_rows.loc[tied_mask].groupby(keys,dropna=False)[value_fields].nunique(dropna=False).gt(1).any(axis=1).sum()) if tied_latest_rows else 0  # 统计并列且数值冲突的披露键
    if conflicting_latest_keys: raise ValueError(f'输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 并列最新值冲突时请先检查数据后再继续
    selected=latest_rows.drop_duplicates(keys+value_fields).drop_duplicates(keys).drop(columns='_version',errors='ignore')  # 仅合并数值完全一致的并列行
    after=int(selected.duplicated(keys).sum()); audit={'duplicate_rows_before':before,'tied_latest_rows':tied_latest_rows,'conflicting_latest_keys':conflicting_latest_keys,'duplicate_rows_after':after,'version_fields':versions}  # 返回并列与冲突检查
    if after: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 选择后仍重复则停止
    return selected,audit  # 返回唯一版本与检查依据

教师参考解答|代码 3

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具绑定规定资产
import pandas as pd  # 导入表格库以标准化HDF字段
import numpy as np  # 导入有限值检查工具
root=Path('/home/ubuntu/r2_data_mount/data/stock')  # 指定股票数据目录
basic_path,financial_path=root/'stock_basic_data.h5',root/'financial_statement.h5'  # 绑定可用HDF基础信息与财报
if not basic_path.exists() or not financial_path.exists(): raise FileNotFoundError('未找到课程股票数据文件,请从课程数据下载入口获取')  # 提示读者下载文件
basic=pd.read_hdf(basic_path,key='stock_basic_info',columns=['order_book_id','symbol','industry_name','province']).rename(columns={'order_book_id':'raw_code','symbol':'name','industry_name':'industry','province':'area'})  # 只读取公司筛选字段并标准化
basic['ts_code']=basic['raw_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 统一证券代码后缀
basic['area']=basic['area'].astype('string').str.replace(r'[省市]$','',regex=True)  # 统一省市简称
required_basic={'ts_code','name','industry','area'}  # 定义公司字段结构
required_financial={'ts_code','end_date','revenue','operate_profit'}  # 定义财报字段结构
agriculture=basic.loc[basic['industry'].str.contains('农|林|牧|渔',na=False) & basic['area'].isin(['上海','江苏','浙江','安徽'])]  # 筛选长三角农业企业
if agriculture.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 空对象时停止
agriculture_raw_codes=agriculture['raw_code'].tolist()  # 保留HDF原始代码用于高效行筛选
financial=pd.read_hdf(financial_path,key='financial_data',where='order_book_id in agriculture_raw_codes',columns=['order_book_id','quarter','info_date','operating_revenue','profit_from_operation']).rename(columns={'order_book_id':'ts_code','quarter':'end_date','info_date':'ann_date','operating_revenue':'revenue','profit_from_operation':'operate_profit'})  # 按对象读取并标准化财报字段
financial['ts_code']=financial['ts_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 统一财报证券代码后缀
financial['end_date']=pd.PeriodIndex(financial['end_date'].str.upper(),freq='Q').end_time.normalize()  # 将季度键转换为报告期末日期
if not required_basic<=set(basic.columns) or not required_financial<=set(financial.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 标准化后仍缺列时停止
financial,version_audit=select_latest_disclosure(financial,['revenue','operate_profit'])  # 统一保留最晚披露版本
panel=financial.merge(agriculture[['ts_code','name','area']],on='ts_code',how='inner',validate='many_to_one').copy()  # 合并公司标签

教师参考解答|代码 4

展开代码(代码区可独立滚动)
if agriculture.empty or panel.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 空对象或空连接时停止
if panel['end_date'].nunique()<4: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 期间不足时停止
if panel['revenue'].eq(0).any() or not np.isfinite(panel[['revenue','operate_profit']].to_numpy()).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 比率分母检查要求
panel['operating_margin']=panel['operate_profit']/panel['revenue']  # 计算经营关系指标
distribution=panel[['revenue','operate_profit','operating_margin']].agg(['count','mean','median','std','skew'])  # 输出分布统计
time_summary=panel.groupby('end_date').agg(companies=('ts_code','nunique'),revenue=('revenue','sum'),median_margin=('operating_margin','median'))  # 输出时间趋势与分母
relationship=panel[['revenue','operate_profit','operating_margin']].corr(method='spearman')  # 输出关系统计
area_check=panel.groupby('area')['revenue'].sum()  # 独立复算地区子集
company_check=panel.loc[panel['ts_code'].eq(panel['ts_code'].iloc[0])].set_index('end_date')['revenue']  # 独立复算首家公司时间序列
print(version_audit,distribution,time_summary,relationship,area_check,company_check)  # 输出数据版本检查与全部新案例依据

教师参考解答|答案与说明 3

  • 解释答案:分布从中心、离散和形态三方面描述样本;偏度与相关不识别异常原因或价格弹性因果。中国财报拓展应用只验证方法,不替代平台农产品订单。
  • 拓展应用答案:平台侧从同一 clean_transactions 同时生成月度与季度订单数/销售额,比较聚合后的行数与波动,并用“月销售额合计=季销售额合计=清洗快照总额”检查核对;这只改变时间粒度,不更换清洗口径。中国财报拓展应用仍只验证方法,不替代订单依据。
  • 参考结果:平台练习包括数据清理、分布、月度与季度趋势、月季总额核对和变量关系;公开教材案例包括长三角农业企业分布、时间汇总、相关关系和地区/公司比较。所需中国市场数据可从课程数据下载入口取得。
  • 边界 / 局限:偏度不等于异常原因;相关不等于价格弹性因果
  • 常见错误:声称臆造的 case 路径是规定资产;只看图不报统计;删除异常不留规则